﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.Synchronization;
using Obra.Common;
using Microsoft.Synchronization.MetadataStorage;
using System.IO;
using System.Diagnostics;

namespace Obra.Sync
{
    /// <summary>
    /// SyncProvider is a simple synchronization provider that utilizes a 
    /// ISyncService to implement functionality.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A simple provider does not support conflict resolution, and assumes 
    /// there are only two providers involved.  Ther is no support for 
    /// peer-to-peer synching.
    /// </para>
    /// <para>
    /// NOTE: the code currently assumes the persistence store is really 
    /// transient, and a transient store means that tombstoning (deletions)
    /// do not exist.
    /// </para>
    /// </remarks>
    public sealed class SyncProvider :
        KnowledgeSyncProvider, 
        INotifyingChangeApplierTarget, 
        IChangeDataRetriever, 
        IDisposable
    {
        private readonly object myLock = new Object();

        public SyncProvider(
            ISyncService syncService,
            string metadataFileName)
        {
            this.syncService = syncService;
            this.metadataFileName = metadataFileName;

            this.CreateSyncIdFormatGroup();
            this.LoadMetadataFile();
        }

        /// <summary>
        /// Load or create a new metadata file.
        /// </summary>
        private void LoadMetadataFile()
        {
            if (File.Exists(this.MetadataFileName))
            {
                this.LoadExistingMetadataStore();
            }
            else
            {
                this.CreateMetadataStore();
            }

            this.replicaId = this.ReplicaMetadata.ReplicaId;
        }

        /// <summary>
        /// Load an existing metadata file.
        /// </summary>
        private void LoadExistingMetadataStore()
        {
            this.metadataStore = SqlMetadataStore.OpenStore(this.MetadataFileName);
            this.replicaMetadata = this.MetadataStore.GetSingleReplicaMetadata();
        }

        /// <summary>
        /// Create a new metadata store.
        /// </summary>
        private void CreateMetadataStore()
        {
            this.metadataStore = SqlMetadataStore.CreateStore(this.MetadataFileName);

            this.replicaMetadata = this.MetadataStore.InitializeReplicaMetadata(
                this.idFormats,
                new SyncId(Guid.NewGuid()),
                new SyncSchema().FieldSchemas,
                null);
        }

        /// <summary>
        /// Create a sync ID format group.
        /// </summary>
        /// <remarks>
        /// <para>
        ///   * Every item in the metadata is represented by a GUID.
        ///   * Replicas matches are determined by matching checksum in string 
        ///   form.  For example, an MD5 string is 32 characters in length.
        /// </para>
        /// <para>
        /// The replica format is currently variable length, and limited to 512
        /// characters, which should be than sufficient for the popular and 
        /// known checksums.
        /// </para>
        /// </remarks>
        private void CreateSyncIdFormatGroup()
        {
            this.idFormats = new SyncIdFormatGroup();

            this.IdFormats.ItemIdFormat.IsVariableLength = true;
            this.IdFormats.ItemIdFormat.Length = SyncSchema.MAX_PATH;

            this.IdFormats.ReplicaIdFormat.IsVariableLength = false;
            this.IdFormats.ReplicaIdFormat.Length = 16; // length of a GUID in bytes.
        }

        private SyncIdFormatGroup idFormats;
        private SyncId replicaId;

        /// <summary>
        /// Replica ID (GUID) for this provider instance.
        /// </summary>
        public SyncId ReplicaId
        {
            get { return this.replicaId; }
        }

        private readonly ISyncService syncService;

        /// <summary>
        /// Synchronization service is a source and target of synchronization.
        /// </summary>
        public ISyncService SyncService
        {
            get { return this.syncService; }
        }

        private readonly string metadataFileName;

        /// <summary>
        /// Metadata file name.
        /// </summary>
        public string MetadataFileName
        {
            get { return this.metadataFileName; }
        }

        private SqlMetadataStore metadataStore;

        /// <summary>
        /// SQL-esque store for item metadatas.
        /// </summary>
        private SqlMetadataStore MetadataStore
        {
            get { return this.metadataStore; }
        }

        private ReplicaMetadata replicaMetadata;

        /// <summary>
        /// Metadata from the <see cref="MetadataStore"/>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The replica metadata is the local (or my) metadata for local media
        /// files.
        /// </para>
        /// </remarks>
        private ReplicaMetadata ReplicaMetadata
        {
            get { return this.replicaMetadata; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="syncId"></param>
        /// <returns></returns>
        internal MediaItemMetadata this[SyncId syncId]
        {
            get
            {
                ItemMetadata itemMetadata = this.ReplicaMetadata.FindItemMetadataById(syncId);
                
                if (itemMetadata == null)
                {
                    return null;
                }
                
                return this.ConvertToMediaItemMetadata(syncId, itemMetadata);
            }
        }

        private SyncSessionContext syncSessionContext;

        /// <summary>
        /// The current status of the sync session.
        /// </summary>
        public SyncSessionContext SyncSessionContext
        {
            get { return this.syncSessionContext; }
        }

        /// <summary>
        /// Wrap an anonymous method in a metadata store transaction.
        /// </summary>
        /// <param name="action">
        /// Anonymous method to invoke inside the transaction.
        /// </param>
        private void TransactMetadataStore(
            Action action)
        {
            this.MetadataStore.BeginTransaction();

            try
            {
                action.Invoke();
            }
            catch (Exception)
            {
                this.MetadataStore.RollbackTransaction();
                throw;
            }
            finally
            {
                this.MetadataStore.CommitTransaction();
            }
        }

        /// <summary>
        /// Cleanup all tombstones older than the specified time span.
        /// </summary>
        /// <param name="timeSpan"></param>
        public void CleanupTombstones(TimeSpan timeSpan)
        {
            this.TransactMetadataStore(
                () => this.ReplicaMetadata.CleanupDeletedItems(timeSpan));
        }

        /// <summary>
        /// Notifies the provider that it is joining a synchronization session.
        /// </summary>
        /// <param name="position">
        /// The position of this provider, relative to the other provider in 
        /// the session.
        /// </param>
        /// <param name="syncSessionContext">
        /// The current status of the corresponding session.
        /// </param>
        public override void BeginSession(
            SyncProviderPosition position,
            SyncSessionContext syncSessionContext)
        {
            this.syncSessionContext = syncSessionContext;
            this.SyncService.Connect();

            this.TransactMetadataStore(
                () => this.UpdateMetadataStoreWithLocalChanges());
        }

        /// <summary>
        /// Notifies the provider that a synchronization session to which it 
        /// was enlisted has completed.
        /// </summary>
        /// <param name="syncSessionContext">
        /// The current status of the corresponding session.
        /// </param>
        public override void EndSession(SyncSessionContext syncSessionContext)
        {
            this.SyncService.Disconnect();
        }

        /// <summary>
        /// Get the sync version.
        /// </summary>
        private SyncVersion SyncVersion
        {
            get { return new SyncVersion(0, this.GetNextTickCount()); }
        }

        /// <summary>
        /// Create a new ItemMetadata based on the supplied MediaItemMetadata.
        /// </summary>
        /// <param name="syncId">
        /// SyncId of the new ItemMetadata.
        /// </param>
        /// <param name="syncVersion">
        /// Sync version of the new ItemMetadata.
        /// </param>
        /// <param name="mediaItemMetadata">
        /// Properties to copy into the new ItemMetadata.
        /// </param>
        /// <returns>
        /// A new ItemMetadata.
        /// </returns>
        /// <remarks>
        /// <para>
        /// The change version is set to the create version.
        /// </para>
        /// </remarks>
        private ItemMetadata CreateItemMetadata(
            SyncId syncId,
            SyncVersion syncVersion,
            MediaItemMetadata mediaItemMetadata)
        {
            ItemMetadata itemMetadata = this.ReplicaMetadata.CreateItemMetadata(
                mediaItemMetadata.SyncId,
                syncVersion);

            itemMetadata.SetCustomField(SyncSchema.CHECKSUM, mediaItemMetadata.Checksum);
            itemMetadata.SetCustomField(SyncSchema.TIMESTAMP, mediaItemMetadata.LastModifiedUtc);

            itemMetadata.ChangeVersion = itemMetadata.CreationVersion;

            return itemMetadata;
        }

        /// <summary>
        /// Add a media item to the replica metadata store.
        /// </summary>
        /// <param name="mediaItemMetadata">
        /// Media item metadata to add to the replica metadata store.
        /// </param>
        private void AddMediaItemToReplicaMetadataStore(
            MediaItemMetadata mediaItemMetadata,
            SyncVersion syncVersion)
        {
            string checksum = this.SyncService.ComputeChecksum(
                mediaItemMetadata);

            mediaItemMetadata.Checksum = checksum;

            ItemMetadata itemMetadata = this.CreateItemMetadata(
                mediaItemMetadata.SyncId,
                syncVersion,
                mediaItemMetadata);

            this.ReplicaMetadata.SaveItemMetadata(
                itemMetadata);
        }

        /// <summary>
        /// Convert an ItemMetadata to a MediaItemMetadata.
        /// </summary>
        /// <param name="itemMetadata">
        /// ItemMetadata to convert from.
        /// </param>
        private MediaItemMetadata ConvertToMediaItemMetadata(
            SyncId syncId,
            ItemMetadata itemMetadata)
        {
            MediaItemMetadata mediaItemMetadata = new MediaItemMetadata()
            {
                Checksum = itemMetadata.GetStringField(SyncSchema.CHECKSUM),
                LastModifiedUtc = (ulong)itemMetadata.GetUInt64Field(SyncSchema.TIMESTAMP),
                Name = syncId.GetStringId(),
            };

            return mediaItemMetadata;
        }

        /// <summary>
        /// Update a media item's metadata if it has changed.
        /// </summary>
        /// <param name="mediaItemMetadata">
        /// </param>
        /// <param name="itemMetadata">
        /// </param>
        /// <returns>
        /// True if the item has been modified since it was last recorded in 
        /// the metadata store.
        /// </returns>
        /// <remarks>
        /// <para>
        /// Rules of the method.
        ///   1. If the item's timestamp has not changed, then the file's 
        ///   contents have not changed, and there is no need to re-compute
        ///   the checksum.  Nothing to do, return.
        /// </para>
        /// </remarks>
        private bool UpdateMediaItemInReplicaMetadataStore(
            MediaItemMetadata mediaItemMetadata,
            ItemMetadata itemMetadata,
            SyncVersion syncVersion)
        {
            MediaItemMetadata currentMediaItemMetadata = this.ConvertToMediaItemMetadata(
                mediaItemMetadata.SyncId,
                itemMetadata);

            // If the item has not been modified since the last synchronization 
            // run, so there is nothing to do.
            // If the item was previously deleted
            if (mediaItemMetadata.LastModifiedUtc == currentMediaItemMetadata.LastModifiedUtc)
            {
                return false;
            }

            string checksum = this.SyncService.ComputeChecksum(
                mediaItemMetadata);

            // The item has changed, so bump the version. 
            itemMetadata.ChangeVersion = syncVersion;

            // Only the checksum and timestamp are (and need to be) updated.
            itemMetadata.SetCustomField(SyncSchema.CHECKSUM, checksum);
            itemMetadata.SetCustomField(SyncSchema.TIMESTAMP, mediaItemMetadata.LastModifiedUtc);

            this.ReplicaMetadata.SaveItemMetadata(
                itemMetadata);

            return true;
        }

        /// <summary>
        /// Delete an item from the metadata store.
        /// </summary>
        /// <param name="itemMetadata">
        /// Item metadata to be deleted.
        /// </param>
        /// <param name="syncVersion">
        /// Sync version of the deleted item.
        /// </param>
        /// <remarks>
        /// <para>
        /// When a item is deleted from the store, the timestamp is zeroed out.
        /// </para>
        /// </remarks>
        private void DeleteMediaItem(
            ItemMetadata itemMetadata,
            SyncVersion syncVersion)
        {
            itemMetadata.ChangeVersion = syncVersion;
            itemMetadata.MarkAsDeleted(syncVersion);
            itemMetadata.SetCustomField(SyncSchema.TIMESTAMP, (ulong)0);
            itemMetadata.SetCustomField(SyncSchema.CHECKSUM, "");

            this.ReplicaMetadata.SaveItemMetadata(itemMetadata);
        }

        /// <summary>
        /// Process a media file that was enumerated by the provider.
        /// </summary>
        /// <param name="mediaItemMetadata">
        /// Metadata for the media item to process.
        /// </param>
        /// <remarks>
        /// <para>
        /// An enumerated media item has one of three possible states.
        ///   1. Media item is new, and does not exist in the metadata store.
        ///   2. Media item is old, but has changed since it was last recorded 
        ///   in the metadata store.
        ///   3. Media item is old, and has not changed since it was last 
        ///   recorded in the metadata store.
        /// </para>
        /// </remarks>
        private void ProcessMediaItem(
            MediaItemMetadata mediaItemMetadata,
            SyncVersion syncVersion)
        {
            ItemMetadata itemMetadata = this.ReplicaMetadata.FindItemMetadataById(
                mediaItemMetadata.SyncId);

            if (itemMetadata == null)
            {
                this.AddMediaItemToReplicaMetadataStore(
                    mediaItemMetadata,
                    syncVersion);
            }

            // If the item has not been modified mark the item as still alive.
            else if (!this.UpdateMediaItemInReplicaMetadataStore(
                    mediaItemMetadata,
                    itemMetadata,
                    syncVersion))
            {
                this.ReplicaMetadata.DeleteDetector.ReportLiveItemById(
                    mediaItemMetadata.SyncId);
            }
        }

        /// <summary>
        /// Update the metadata store with media files from the SyncService.
        /// </summary>
        private void UpdateMetadataStoreWithLocalChanges()
        {
            SyncVersion newSyncVersion = this.SyncVersion;

            this.ReplicaMetadata.DeleteDetector.MarkAllItemsUnreported();

            List<MediaItemMetadata> mediaFiles = this.SyncService.EnumerateMediaFiles();

            // Iterate over all of the changes from this providers.
            foreach (var mediaItemMetadata in mediaFiles)
            {
                this.ProcessMediaItem(mediaItemMetadata, newSyncVersion);
            }

            // Iterate over all items that are in the metadata store that
            // were _not_ enumerated by the SyncService.EnumerateMediaFiles().
            foreach (ItemMetadata itemMetadata in this.ReplicaMetadata.DeleteDetector.FindUnreportedItems())
            {                
                this.DeleteMediaItem(itemMetadata, newSyncVersion);
            }
        }

        /// <summary>
        /// Gets a change batch that contains item metadata for items that are 
        /// not contained in the specified knowledge from the destination 
        /// provider.
        /// </summary>
        /// <param name="batchSize">
        /// The number of changes to include in the change batch.
        /// </param>
        /// <param name="destinationKnowledge">
        /// The knowledge from the destination provider. This knowledge must 
        /// be mapped by calling <see cref="M:Microsoft.Synchronization.SyncKnowledge.MapRemoteKnowledgeToLocal(Microsoft.Synchronization.SyncKnowledge)"/> 
        /// on the source knowledge before it can be used for change enumeration.</param>
        /// <param name="changeDataRetriever">
        /// Returns an object that can be used to retrieve change data. It can 
        /// be an <see cref="T:Microsoft.Synchronization.IChangeDataRetriever"/> 
        /// object or a provider-specific object.
        /// </param>
        /// <returns>
        /// A change batch that contains item metadata for items that are not 
        /// contained in the specified knowledge from the destination provider. 
        /// Cannot be a null.
        /// </returns>
        public override ChangeBatch GetChangeBatch(
            uint batchSize,
            SyncKnowledge destinationKnowledge,
            out object changeDataRetriever)
        {
            changeDataRetriever = this.GetDataRetriever();

            ChangeBatch changeBatch = this.ReplicaMetadata.GetChangeBatch(
                batchSize,
                destinationKnowledge);

            return changeBatch;
        }

        /// <summary>
        /// Gets an object that can be used to retrieve item data from a replica.
        /// </summary>
        /// <returns>
        /// An object that can be used to retrieve item data from a replica.
        /// </returns>
        public IChangeDataRetriever GetDataRetriever()
        {
            return this;
        }

        /// <summary>
        /// Gets a change batch that contains item metadata for items that have 
        /// IDs greater than the specified lower bound, as part of a full enumeration.
        /// </summary>
        /// <param name="batchSize">
        /// The number of changes to include in the change batch.
        /// </param>
        /// <param name="lowerEnumerationBound">
        /// The lower bound for item IDs. This method returns changes that have 
        /// IDs greater than or equal to this ID value.
        /// </param>
        /// <param name="knowledgeForDataRetrieval">
        /// If an item change is contained in this knowledge object, data for 
        /// that item already exists on the destination replica.
        /// </param>
        /// <param name="changeDataRetriever">
        /// Returns an object that can be used to retrieve change data. It can 
        /// be an <see cref="T:Microsoft.Synchronization.IChangeDataRetriever"/> 
        /// object or a be provider-specific object.
        /// </param>
        /// <returns>
        /// A change batch that contains item metadata for items that have IDs 
        /// greater than the specified lower bound, as part of a full enumeration.
        /// </returns>
        public override FullEnumerationChangeBatch GetFullEnumerationChangeBatch(
            uint batchSize,
            SyncId lowerEnumerationBound,
            SyncKnowledge knowledgeForDataRetrieval,
            out object changeDataRetriever)
        {
            FullEnumerationChangeBatch changeBatch = this.ReplicaMetadata.GetFullEnumerationChangeBatch(
                batchSize,
                lowerEnumerationBound,
                knowledgeForDataRetrieval);

            changeDataRetriever = this;
            return changeBatch;
        }

        // XXX: arbitrary, probably should be configurable.
        private const uint SYNC_PROVIDER_BATCH_SIZE = 1;

        /// <summary>
        /// Gets the number of item changes that will be included in change 
        /// batches, and the current knowledge for the synchronization scope.
        /// </summary>
        /// <param name="batchSize">
        /// The number of item changes that will be included in change batches 
        /// returned by this object.
        /// </param>
        /// <param name="knowledge">
        /// The current knowledge for the synchronization scope, or a newly 
        /// created knowledge object if no current knowledge exists.
        /// </param>
        public override void GetSyncBatchParameters(
            out uint batchSize,
            out SyncKnowledge knowledge)
        {
            batchSize = SYNC_PROVIDER_BATCH_SIZE;
            knowledge = this.ReplicaMetadata.GetKnowledge();
        }

        /// <summary>
        /// Processes a set of changes by detecting conflicts and applying 
        /// changes to the item store.
        /// </summary>
        /// <param name="resolutionPolicy">
        /// The conflict resolution policy to use when this method applies 
        /// changes.
        /// </param>
        /// <param name="sourceChanges">
        /// A batch of changes from the source provider to be applied locally.
        /// </param>
        /// <param name="changeDataRetriever">
        /// An object that can be used to retrieve change data. It can be an 
        /// <see cref="T:Microsoft.Synchronization.IChangeDataRetriever"/> 
        /// object or a provider-specific object.
        /// </param>
        /// <param name="syncCallbacks">
        /// An object that receives event notifications during change 
        /// application.
        /// </param>
        /// <param name="sessionStatistics">
        /// Tracks change statistics. For a provider that uses custom change 
        /// application, this object must be updated with the results of the 
        /// change application.
        /// </param>
        public override void ProcessChangeBatch(
            ConflictResolutionPolicy resolutionPolicy,
            ChangeBatch sourceChanges,
            object changeDataRetriever,
            SyncCallbacks syncCallbacks,
            SyncSessionStatistics sessionStatistics)
        {
            this.TransactMetadataStore(delegate()
            {
                var localVersions = this.ReplicaMetadata.GetLocalVersions(sourceChanges);

                NotifyingChangeApplier notifyChangeApplier = new NotifyingChangeApplier(
                    this.IdFormats);

                notifyChangeApplier.ApplyChanges(
                    resolutionPolicy,
                    sourceChanges,
                    changeDataRetriever as IChangeDataRetriever,
                    localVersions,
                    this.ReplicaMetadata.GetKnowledge(),
                    this.ReplicaMetadata.GetForgottenKnowledge(),
                    this/*INotifyChangeApplierTarget*/,
                    this.SyncSessionContext,
                    syncCallbacks);
            });
        }

        /// <summary>
        /// Processes a set of changes for a full enumeration by applying 
        /// changes to the item store.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Full enumeration is not implemented by this synchronization
        /// provider.
        /// </para>
        /// </remarks>
        public override void ProcessFullEnumerationChangeBatch(
            ConflictResolutionPolicy resolutionPolicy,
            FullEnumerationChangeBatch sourceChanges,
            object changeDataRetriever,
            SyncCallbacks syncCallbacks,
            SyncSessionStatistics sessionStatistics)
        {
            this.TransactMetadataStore(delegate()
            {
                IEnumerable<ItemChange> localChanges = this.ReplicaMetadata.GetFullEnumerationLocalVersions(
                    sourceChanges);

                NotifyingChangeApplier changeApplier = new NotifyingChangeApplier(this.IdFormats);

                changeApplier.ApplyFullEnumerationChanges(
                    resolutionPolicy,
                    sourceChanges,
                    changeDataRetriever as IChangeDataRetriever,
                    localChanges,
                    this.ReplicaMetadata.GetKnowledge(),
                    this.ReplicaMetadata.GetForgottenKnowledge(),
                    this,
                    this.SyncSessionContext,
                    syncCallbacks);

            });
        }

        #region INotifyingChangeApplierTarget Members

        /// <summary>
        /// Gets an object that can be used to retrieve item data from a 
        /// replica.
        /// </summary>
        /// <returns>
        /// An object that can be used to retrieve item data from a replica.
        /// </returns>
        IChangeDataRetriever INotifyingChangeApplierTarget.GetDataRetriever()
        {
            return this;
        }

        /// <summary>
        /// Increments the tick count and returns the new tick count.
        /// </summary>
        /// <returns>
        /// The newly incremented tick count.
        /// </returns>
        public ulong GetNextTickCount()
        {
            return this.ReplicaMetadata.GetNextTickCount();
        }

        /// <summary>
        /// Gets the ID format schema of the provider.
        /// </summary>
        /// <returns>
        /// The ID format schema of the provider.
        /// </returns>
        public override SyncIdFormatGroup IdFormats
        {
            get { return this.idFormats; }
        }

        /// <summary>
        /// Saves an item change that contains unit change changes to the item 
        /// store.
        /// </summary>
        void INotifyingChangeApplierTarget.SaveChangeWithChangeUnits(
            ItemChange change,
            SaveChangeWithChangeUnitsContext context)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Saves information about a change that caused a conflict.
        /// </summary>
        void INotifyingChangeApplierTarget.SaveConflict(
            ItemChange conflictingChange,
            object conflictingChangeData,
            SyncKnowledge conflictingChangeKnowledge)
        {
            throw new NotImplementedException();
        }

        #region SaveChangeAction

        /// <summary>
        /// Save a media item to the sync service.
        /// </summary>
        /// <param name="mediaItemMetadata">
        /// Media item metadata for the media file to save.
        /// </param>
        /// <param name="itemChange">
        /// Media item change information to commit to the metadata store.
        /// </param>
        private void SaveChangeAction_Create(
            ISyncItemTransfer syncItemTransfer,
            ItemChange itemChange)
        {
            this.SyncService.SaveChange(
                syncItemTransfer);

            ItemMetadata itemMetadata = this.CreateItemMetadata(
                itemChange.ItemId,
                itemChange.CreationVersion,
                syncItemTransfer.MediaItemMetadata);

            this.ReplicaMetadata.SaveItemMetadata(itemMetadata);
        }

        /// <summary>
        /// Delete an item from the service provider.
        /// </summary>
        /// <param name="mediaItemMetadata">
        /// Media item metadata for the media file to save.
        /// </param>
        /// <param name="itemChange">
        /// Media item change information to commit to the metadata store.
        /// </param>
        /// <remarks>
        /// <para>
        /// 
        /// </para>
        /// </remarks>
        private void SaveChangeAction_DeleteAndStoreTombstone(
            ISyncItemTransfer syncItemTransfer,
            ItemChange itemChange)
        {
            ItemMetadata itemMetadata = this.ReplicaMetadata.FindItemMetadataById(
                itemChange.ItemId);

            if (itemMetadata == null)
            {
                itemMetadata = this.ReplicaMetadata.CreateItemMetadata(
                    itemChange.ItemId,
                    itemChange.ChangeVersion);
            }

            switch (itemChange.ChangeKind)
            {
                case ChangeKind.Deleted:
                    itemMetadata.MarkAsDeleted(itemChange.ChangeVersion);
                    break;
                default:
                    string message = String.Format(Resource.ChangeKindNotSupportedTemplate,
                        itemChange.ChangeKind,
                        syncItemTransfer.MediaItemMetadata.Name);

                    throw new ArgumentException(message);
            }

            this.SyncService.DeleteMediaFile(
                syncItemTransfer.MediaItemMetadata);

            itemMetadata.ChangeVersion = itemChange.ChangeVersion;

            this.ReplicaMetadata.SaveItemMetadata(
                itemMetadata);
        }

        /// <summary>
        /// Merge a delete between the two providers.
        /// </summary>
        /// <param name="itemChange">
        /// The item change to merge.
        /// </param>
        /// <param name="itemMetadata">
        /// Metadata for the item to merge.
        /// </param>
        /// <param name="myMediaItemMetadata">
        /// My media item metadata for the change to be merged.
        /// </param>
        private void SaveChangeAction_MergeDeleted(
            ItemChange itemChange,
            ItemMetadata itemMetadata,
            MediaItemMetadata myMediaItemMetadata)
        {
            itemMetadata.MarkAsDeleted(itemChange.ChangeVersion);
            itemMetadata.SetCustomField(SyncSchema.TIMESTAMP, (ulong)0);
            itemMetadata.SetCustomField(SyncSchema.CHECKSUM, "");

            this.SyncService.DeleteMediaFile(
                myMediaItemMetadata);
        }

        /// <summary>
        /// Merge data between the two providers.
        /// </summary>
        /// <param name="syncItemTransfer">
        /// Object containing information and data about the media file to
        /// merge t othis provider.
        /// </param>
        /// <param name="itemChange">
        /// The item change to merge.
        /// </param>
        /// <param name="itemMetadata">
        /// Metadata for item to merge.
        /// </param>
        /// <param name="myMediaItemMetadata">
        /// My media item metadata for the change to be merged.
        /// </param>
        private void SaveChangeAction_MergeData(
            ISyncItemTransfer syncItemTransfer,
            ItemChange itemChange,
            ItemMetadata itemMetadata,
            MediaItemMetadata myMediaItemMetadata)
        {
            MediaItemMetadata hisMediaItemMetadata = syncItemTransfer.MediaItemMetadata;

            // Only update the media file if the checksum has changed.
            if (myMediaItemMetadata.Checksum != hisMediaItemMetadata.Checksum)
            {
                itemMetadata.SetCustomField(SyncSchema.CHECKSUM, hisMediaItemMetadata.Checksum);

                this.SyncService.SaveChange(
                    syncItemTransfer);
            }

            itemMetadata.ChangeVersion = itemChange.ChangeVersion;
            itemMetadata.SetCustomField(SyncSchema.TIMESTAMP, hisMediaItemMetadata.LastModifiedUtc);
        }

        /// <summary>
        /// Update the media file on this service provider.
        /// </summary>
        /// <param name="mediaItemMetadata">
        /// Media item metadata for the media file to save.
        /// </param>
        /// <param name="itemChange">
        /// Media item change information to commit to the metadata store.
        /// </param>
        /// <remarks>
        /// <para>
        /// The version is always updated, but the file is only transferred if
        /// the file's checksum has changed.
        /// </para>
        /// <para>
        /// This method is always called when resolving conflict because the
        /// conflict resolution is custom, and it is set to Merge.
        /// </para>
        /// </remarks>
        private void SaveChangeAction_UpdateVersionAndMergeData(
            ISyncItemTransfer syncItemTransfer,
            ItemChange itemChange)
        {
            ItemMetadata itemMetadata = this.ReplicaMetadata.FindItemMetadataById(
                itemChange.ItemId);

            MediaItemMetadata myMediaItemMetadata = this.ConvertToMediaItemMetadata(
                itemChange.ItemId,
                itemMetadata);

            switch (itemChange.ChangeKind)
            {
                case ChangeKind.Deleted:
                    this.SaveChangeAction_MergeDeleted(
                        itemChange,
                        itemMetadata,
                        myMediaItemMetadata);
                    break;
                default:
                    this.SaveChangeAction_MergeData(
                        syncItemTransfer,
                        itemChange,
                        itemMetadata,
                        myMediaItemMetadata);
                    break;
            }

            this.ReplicaMetadata.SaveItemMetadata(
                itemMetadata);
        }

        #endregion SaveChangeAction

        /// <summary>
        /// Saves an item change to the item store.
        /// </summary>
        /// <param name="saveChangeAction">
        /// The action to be performed for the change.
        /// </param>
        /// <param name="change">
        /// The item change to save.
        /// </param>
        /// <param name="context">
        /// Information about the change to be applied.
        /// </param>
        /// <remarks>
        /// <para>
        /// This code executes inside the metadata store transaction begun in
        /// <see cref="ProcessChangeBatch"/>.
        /// </para>
        /// </remarks>
        void INotifyingChangeApplierTarget.SaveItemChange(
            SaveChangeAction saveChangeAction,
            ItemChange change,
            SaveChangeContext context)
        {
            ISyncItemTransfer syncItemTransfer = (ISyncItemTransfer)context.ChangeData;

            switch (saveChangeAction)
            {
                case SaveChangeAction.Create:
                    this.SaveChangeAction_Create(syncItemTransfer, change);
                    break;
                case SaveChangeAction.UpdateVersionAndMergeData:
                    // This SaveChangeAction is always called if the conflict resolution
                    // is set to Merge.
                    this.SaveChangeAction_UpdateVersionAndMergeData(syncItemTransfer, change);
                    break;
                default:
                    string message = String.Format(Resource.SaveChangeActionNotSupportedTemplate,
                        saveChangeAction.ToString());

                    throw new ArgumentException(message);
            }
        }

        /// <summary>
        /// Stores the knowledge for the current scope.
        /// </summary>
        /// <param name="knowledge">
        /// </param>
        /// <param name="forgottenKnowledge">
        /// The forgotten knowledge to be saved.
        /// </param>
        /// <remarks>
        /// <para>
        /// This code executes inside a metadata store transaction opened in
        /// <see cref="ProcessBatchChange"/>.
        /// </para>
        /// </remarks>
        void INotifyingChangeApplierTarget.StoreKnowledgeForScope(
            SyncKnowledge knowledge,
            ForgottenKnowledge forgottenKnowledge)
        {
            this.ReplicaMetadata.SetKnowledge(knowledge);
            this.ReplicaMetadata.SetForgottenKnowledge(forgottenKnowledge);
            this.ReplicaMetadata.SaveReplicaMetadata();
        }

        /// <summary>
        /// Gets the version of an item stored in the destination replica.
        /// </summary>
        /// <param name="sourceChange">
        /// The item change that is sent by the source provider.</param>
        /// <param name="destinationVersion">
        /// Returns an item change that contains the version of the item in the
        /// destination replica.
        /// </param>
        /// <returns>
        /// true if the item was found in the destination replica; otherwise, 
        /// false.
        /// </returns>
        bool INotifyingChangeApplierTarget.TryGetDestinationVersion(
            ItemChange sourceChange,
            out ItemChange destinationVersion)
        {
            destinationVersion = null;
            return false;
        }

        #endregion

        #region IChangeDataRetriever Members

        /// <summary>
        /// This method retrieves item data for a change.
        /// </summary>
        /// <param name="loadChangeContext">
        /// Metadata that describes the change for which data should be 
        /// retrieved.
        /// </param>
        /// <returns>The item data for the change.</returns>
        object IChangeDataRetriever.LoadChangeData(
            LoadChangeContext loadChangeContext)
        {
            string mediaFileName = loadChangeContext.ItemChange.ItemId.GetStringId();

            return this.SyncService.LoadChange(
                mediaFileName);
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (this.MetadataStore != null)
            {
                this.MetadataStore.Dispose();
            }
        }

        #endregion
    }
}
